import json
import time
from types import TracebackType
from typing import Optional, List, Type
from urllib import parse

import aiofiles
import aiohttp
import telegram


class GLaDOSCheckIn:
    _HOST: str = "glados.one"
    _ORIGIN_URL: str = f"https://{_HOST}"
    # noinspection SpellCheckingInspection
    _UA: str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.188"
    _BUDGET_DATA_PATH: str = "budget.json"
    _TOKENS: List[str] = [
        "glados.one",
        "glados.network",
        "glados.space",
        "glados.vip",
        "glados.eu.org",
    ]

    def __init__(self, cookies: str) -> None:
        self._cookies: str = cookies
        self.client: aiohttp.ClientSession = aiohttp.ClientSession(raise_for_status=True)

    async def _show_msg(self, msg: str) -> None:
        print(msg)

    async def _report_success(
            self, msg: str, left_days: int, plan: str, used_gb: float, total_gb: int
    ) -> None:
        await self._show_msg(
            "GLaDOS CheckIn\n"
            + f"Msg: {msg}\n"
            + f"Plan: {plan} Plan\n"
            + f"Left days: {left_days}\n"
            + f"Usage: {used_gb:.3f}GB\n"
            + f"Total: {total_gb}GB\n"
            + f"Time: {time.asctime()}"
        )

    async def _report_error(self, msg: str) -> None:
        await self._show_msg(
            "GLaDOS CheckIn\n"
            + f"Msg: {msg}\n"
            + f"Time: {time.asctime()}\n"
        )

    async def _api_traffic(self) -> dict:
        traffic_url = f"{self._ORIGIN_URL}/api/user/traffic"
        referer_url = f"{self._ORIGIN_URL}/console"

        async with self.client.get(
                traffic_url,
                headers={
                    "cookie": self._cookies,
                    "referer": referer_url,
                    "origin": self._ORIGIN_URL,
                    "user-agent": self._UA,
                    "content-type": "application/json;charset=UTF-8",
                    "accept-encoding": "gzip, deflate",
                },
        ) as r:
            return await r.json()

    async def _api_check_in(self) -> dict:
        check_in_url = f"{self._ORIGIN_URL}/api/user/checkin"
        referer_url = f"{self._ORIGIN_URL}/console/checkin"

        for token in self._TOKENS:
            async with self.client.post(
                    check_in_url,
                    headers={
                        "cookie": self._cookies,
                        "referer": referer_url,
                        "origin": self._ORIGIN_URL,
                        "user-agent": self._UA,
                        "content-type": "application/json;charset=UTF-8",
                        "accept-encoding": "gzip, deflate",
                    },
                    json={"token": token},
            ) as r:
                response = await r.json()
                assert "message" in response
                if "token error" not in response["message"]:
                    return response

    async def _api_status(self) -> dict:
        status_url = f"{self._ORIGIN_URL}/api/user/status"
        referer_url = f"{self._ORIGIN_URL}/console/checkin"

        async with self.client.get(
                status_url,
                headers={
                    "cookie": self._cookies,
                    "referer": referer_url,
                    "origin": self._ORIGIN_URL,
                    "user-agent": self._UA,
                    "accept-encoding": "gzip, deflate",
                },
        ) as r:
            return await r.json()

    async def _get_budget(self, vip_level: Optional[int]) -> dict:
        async with aiofiles.open(self._BUDGET_DATA_PATH, "r") as f:
            budget_info = json.loads(await f.read())
            user_budgets = list(filter(
                lambda i: (vip_level is not None and "vip" in i and i["vip"] == vip_level) or (vip_level is None and "vip" not in i),
                budget_info
            ))
            if len(user_budgets) > 0:
                return user_budgets[0]
            else:
                raise ValueError(f"Budget info not found for this user! VIP: {vip_level}")

    async def _check_in(self) -> None:
        check_in_response = await self._api_check_in()
        check_in_msg = check_in_response["message"]

        if check_in_msg == "\u6ca1\u6709\u6743\u9650":
            await self._report_error("Cookies expired!")
            return

        status_response = await self._api_status()
        left_days = int(status_response["data"]["leftDays"].split(".")[0])
        vip_level = status_response["data"]["vip"]

        traffic_response = await self._api_traffic()
        used_gb = traffic_response["data"]["today"] / 1024 / 1024 / 1024

        user_budget = await self._get_budget(vip_level)
        total_gb = user_budget["budget"]
        plan = user_budget["level"]

        await self._report_success(check_in_msg, left_days, plan, used_gb, total_gb)

    # noinspection PyBroadException
    async def run_check_in(self) -> None:
        try:
            await self._check_in()
        except BaseException as e:
            await self._report_error(str(e))

    def __enter__(self) -> None:
        raise TypeError("Use async with instead")

    def __exit__(
            self,
            exc_type: Optional[Type[BaseException]],
            exc_val: Optional[BaseException],
            exc_tb: Optional[TracebackType],
    ) -> None:
        # __exit__ should exist in pair with __enter__ but never executed
        pass  # pragma: no cover

    async def __aenter__(self) -> "GLaDOSCheckIn":
        return self

    async def __aexit__(
            self,
            exc_type: Optional[Type[BaseException]],
            exc_val: Optional[BaseException],
            exc_tb: Optional[TracebackType],
    ) -> None:
        # noinspection PyBroadException
        try:
            await self.client.close()
        except:
            pass


class GLaDOSCheckInTgBot(GLaDOSCheckIn):
    def __init__(self, cookies: str, bot_token: str, chat_id: str) -> None:
        super().__init__(cookies)
        self._chat_id: str = chat_id
        self._bot: telegram.Bot = telegram.Bot(token=bot_token)

    async def _show_msg(self, msg: str) -> None:
        await self._bot.send_message(self._chat_id, msg)

    async def __aenter__(self) -> "GLaDOSCheckInTgBot":
        return self

    async def __aexit__(
            self,
            exc_type: Optional[Type[BaseException]],
            exc_val: Optional[BaseException],
            exc_tb: Optional[TracebackType],
    ) -> None:
        await super().__aexit__(exc_type, exc_val, exc_tb)
        # noinspection PyBroadException
        try:
            await self._bot.shutdown()
        except:
            pass


class GLaDOSCheckInWebHook(GLaDOSCheckIn):
    def __init__(self, cookies: str, webhook_url: str, token: str) -> None:
        super().__init__(cookies)
        self._webhook_url: str = webhook_url
        self._token: str = token

    async def _show_msg(self, msg: str) -> None:
        (await self.client.post(
            self._webhook_url,
            headers={
                "Token": self._token
            },
            json={
                "msg": msg
            }
        )).close()


class GLaDOSCheckInWeChat(GLaDOSCheckIn):
    _TOKEN_URL = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken"
    _SEND_MSG_URL = f"https://qyapi.weixin.qq.com/cgi-bin/message/send"

    def __init__(self, cookies: str, corp_id: str, agent_id: str, corp_secret: str, to_user: str) -> None:
        super().__init__(cookies)
        self._corp_id: str = corp_id
        self._agent_id: str = agent_id
        self._to_user: str = to_user
        self._corp_secret: str = corp_secret
        self._access_token: Optional[str] = None

    async def _get_access_token(self) -> str:
        url_query = parse.urlencode({
            "corpid": self._corp_id,
            "corpsecret": self._corp_secret
        })
        async with self.client.get(f"{self._TOKEN_URL}?{url_query}") as r:
            content = await r.json()
        if content["errcode"] != 0:
            raise ValueError(content["errmsg"])
        return content["access_token"]

    async def _send_msg(self, msg: str) -> None:
        if self._access_token is None:
            self._access_token = await self._get_access_token()
        assert self._access_token is not None
        url_query = parse.urlencode({
            "access_token": self._access_token
        })
        # noinspection SpellCheckingInspection
        post_json = {
            "touser": self._to_user,
            "msgtype": "text",
            "agentid": self._agent_id,
            "text": {
                "content": msg
            }
        }
        async with self.client.post(f"{self._SEND_MSG_URL}?{url_query}", json=post_json) as r:
            content = await r.json()
        if content["errcode"] != 0:
            raise ValueError(content["errmsg"])

    async def _show_msg(self, msg: str) -> None:
        await self._send_msg(msg)
